/** 
 *  @file server.c
 *  @brief Geolocation Project - Server Code - ENSEIRB RSR 2007, 
 *  everything is under GNU GPL v3 or later
 *  @author Jean Loup Le Roux
 *  @author http://www.nightmare-creature.org
 *
 */

#include "../common.h"
#include "../protocol_visu.h"
#include "../protocol_client.h"
#include "../protocol_servers_config.h"
#include "server.h"

#include <arpa/inet.h>
#include <netdb.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <sys/types.h>
#include <sys/time.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <time.h>
#include <signal.h>
#include <sys/wait.h>
#include <fcntl.h>

#define ERROR(msg) fprintf(stderr, "%s in %s in %s:%d\n", msg, __FUNCTION__, __FILE__, __LINE__)

int go_clean_exit = -1;

void signal_handler( int sig ) {
  if ( sig == SIGINT || sig == SIGTERM ){
    if( go_clean_exit != 0 ){ 
      go_clean_exit = 0; 
      fprintf(stderr, " Caught SIGINT or SIGTERM, will try to exit ASAP!\n");
    }
    else {
      fprintf(stderr, " Please wait, network or disk might be frozen.\n");
    }
  }
}

int main( int argc, char** argv ) {

  int on = 1;

  fprintf(stderr, SERVER_BANNER, __TIME__, __DATE__);

  signal(SIGTERM, signal_handler);
  signal(SIGINT, signal_handler);


  // _________________ PROCESSING ARGC ARGV ____________________ok


  char* config_file = NULL;
  int tmp_my_srv_id = -1;

  if( argc != CORRECT_ARGC ){
    Server_DisplayHelp(); 
    return(0);
  }
  else {
    config_file = argv[1];
    tmp_my_srv_id = atoi( argv[2] );
  }


  // __________________ SERVER DEFINITIONS ______________________ok


  Server srv_array[MAX_SUPPORTED_SRV];
  int it_srv_cfg_loop = 0;
  int srv_cfg_ret = -1;
  int max_srv_id = -1;
  int my_srv_id = -1;
  fd_set read_fds;
  FILE* cfg_file = NULL;
  FD_ZERO( &read_fds );

  fprintf(stderr, " Handling %d maximum servers\n", MAX_SUPPORTED_SRV);

  cfg_file = fopen( (const char*) config_file, "r"); 

  if( cfg_file == NULL ){
    ERROR("no servers config file found");
    return(-1);
  }

  for( it_srv_cfg_loop = 0; it_srv_cfg_loop < MAX_SUPPORTED_SRV; it_srv_cfg_loop++ ){

    srv_cfg_ret = Server_ReadConfigFile( (FILE*) cfg_file, &srv_array[ it_srv_cfg_loop ] );

    if( srv_cfg_ret < 0 ){

      // bad config file protection
      if( it_srv_cfg_loop == 0 ){
        ERROR("invalid servers config file");
        return(-1);
      }

      break;
    }

  }

  fclose(cfg_file);

  max_srv_id = it_srv_cfg_loop; 

  if( ( tmp_my_srv_id < 0 ) || ( tmp_my_srv_id >= max_srv_id ) ){
    fprintf(stderr, " Unavailable server ID. I'm server #0 instead.\n");
    my_srv_id = 0;
  }
  else {
    my_srv_id = tmp_my_srv_id;
  }

  Server_SharedZoneWeaver( srv_array, my_srv_id, max_srv_id ); 


  // _______________ CLIENT SLOTS INITIALISATION ___________________ok


  int max_client_id = MAX_SUPPORTED_SRV * MAX_CLIENTS_PER_SERVER;

  Client cli_array[ max_client_id ];

  fprintf(stderr, " Handling %d maximum clients\n", max_client_id);
  fprintf(stderr, " Client timeout : %d seconds\n", CLIENT_OUT_TIMEOUT); 

  Server_InitClientArray( cli_array, max_client_id );


  // __________________ DISPLAYING SERVERS ________________ok


  int it_srvid = -1;

  fprintf(stderr, 
          " According to configuration file, I'm server #%d and my public IPv4 address is %s\n", 
          my_srv_id, 
          srv_array[my_srv_id].ip);

  for( it_srvid=0; it_srvid<max_srv_id; it_srvid++){
    // Server_Print(&srv_array[it_srvid]);
  }


  // ___________________ MASTER TCP SOCKET ____________________ok


  int master_tcp = -1;
  struct sockaddr_in master_tcp_addr;

  master_tcp = socket(AF_INET, SOCK_STREAM, SELECT_PROTOCOL);

  if( master_tcp == -1) {
    ERROR("socket() error");
    return(-1);
  }

  master_tcp_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  master_tcp_addr.sin_family = AF_INET;
  master_tcp_addr.sin_port = htons( PROTOCOL_SERVERS_TCP_PORT );

  if ( setsockopt( master_tcp, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on) ) < 0 )
    ERROR("setsockopt() error");

  if( bind( master_tcp, (const struct sockaddr*) &master_tcp_addr, sizeof(master_tcp_addr) ) == -1 ) {
    ERROR("bind() error");
    return (-1);
  }

  if( listen( master_tcp, LISTEN_PENDING_TCP ) == -1 ) {
    ERROR("listen() error");
    return(-1);
  }

  Server_FlagMySocket( master_tcp, O_NONBLOCK );

  fprintf(stderr, " Server server (TCP port %d) now ready\n", PROTOCOL_SERVERS_TCP_PORT);


  // ________________ MASTER UDP SOCKET __________________ok


  int master_udp = -1;
  struct sockaddr_in master_udp_addr;

  master_udp = socket(AF_INET, SOCK_DGRAM, SELECT_PROTOCOL);

  if( master_udp == -1) {
    ERROR("socket() error");
    return(-1);
  }

  master_udp_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  master_udp_addr.sin_family = AF_INET;
  master_udp_addr.sin_port = htons( PROTOCOL_SERVERS_UDP_PORT );

  if ( setsockopt( master_udp, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on) ) < 0 )
    ERROR("setsockopt() error");

  if( bind( master_udp, (const struct sockaddr*) &master_udp_addr, sizeof(master_udp_addr) ) == -1 ) {
    ERROR("bind() error");
    return (-1);
  }

  fprintf(stderr, " Client server (UDP port %d) now ready\n", PROTOCOL_SERVERS_UDP_PORT); 


  // ___________________ VISU TCP SOCKET ____________________ok


  int master_visu_tcp = -1;
  int visu_tcp = -1;
  int visu_tcp_ready = INVALID_CLIENT;
  struct sockaddr_in master_visu_tcp_addr;

  master_visu_tcp = socket(AF_INET, SOCK_STREAM, SELECT_PROTOCOL);

  if( master_visu_tcp == -1) {
    ERROR("socket() error");
    return(-1);
  }

  master_visu_tcp_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  master_visu_tcp_addr.sin_family = AF_INET;
  master_visu_tcp_addr.sin_port = htons( VISU_SERVER_TCP_PORT );

  if ( setsockopt( master_visu_tcp, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on) ) < 0 )
    ERROR("setsockopt() error");

  if( bind( master_visu_tcp, (const struct sockaddr*) &master_visu_tcp_addr, sizeof(master_visu_tcp_addr) ) == -1 ) {
    ERROR("bind() error");
    return (-1);
  }

  if( listen( master_visu_tcp, LISTEN_PENDING_TCP ) == -1 ) {
    ERROR("listen() error");
    return(-1);
  }

  Server_FlagMySocket( master_visu_tcp, O_NONBLOCK );

  fprintf(stderr, " Visualisation server (TCP port %d) now ready\n", VISU_SERVER_TCP_PORT);


  //___________ TCP LINKING WITH NEIGHBOURS ____________ok


  int it_j = -1;
  int srv_to_link_id = -1;

  // listing my neighbours
  for( it_j=0; it_j<MAX_NGB; it_j++ ){

    srv_to_link_id = srv_array[my_srv_id].ngb[it_j];

    if( srv_to_link_id == -1 ) continue;

    if( Server_NgbTcpLink( &srv_array[my_srv_id], &srv_array[srv_to_link_id] ) == -1 ){
      ERROR("failed to contact neighbour (skipping)");
    }
  }


  // ____________ SHAKEN, NOT STIRRED aka MAIN SELECT() LOOP ____________


  int select_ret, it_i, it_p, select_ngb, fdset_ngb, it_update_cli;
  int fillret = -1;
  int packret = -1; 
  int updateret = -1;
  struct timeval select_timeout;
  struct timeval server_time;

  // main loop
  while( go_clean_exit != 0 ){


    // ____________ EVERY SELECT() LOOP ROUTINES _____________ok


    // Refreshing OUT Clients
    for( it_update_cli = 0; it_update_cli < max_client_id; it_update_cli++ ){

      if( gettimeofday( &server_time, NULL ) < 0 ){
        ERROR("gettimeofday() failed");
        return(-1);
      }

      // not OUT
      if( cli_array[it_update_cli].status != CSTATUS_OUT ){ 

        if( ( cli_array[it_update_cli].update_time + CLIENT_OUT_TIMEOUT ) < server_time.tv_sec ){
          cli_array[it_update_cli].status = CSTATUS_OUT;
          fprintf(stderr," Client '%s' is now OUT!\n", cli_array[it_update_cli].mac);
        }

      }

    }

    if( ( visu_tcp != -1 ) && ( visu_tcp_ready == VALID_CLIENT ) ){

      for( it_update_cli=0; it_update_cli < max_client_id; it_update_cli++ ){

        if( ( cli_array[it_update_cli].time != INVALID_CLIENT_TIME ) && 
            ( cli_array[it_update_cli].timeout != INVALID_CLIENT_TIME ) ) {

          if( Server_UpdateVisufromClient( visu_tcp, cli_array, it_update_cli ) > -1 ){
            it_update_cli++; it_update_cli--;
          }
          else
            ERROR("could not send Client's status to visu client");
        }
      }
    }

    // Server_PrintClients( cli_array, max_client_id );


    // _____________ FD SET STUFF ________________ok


    // (re)adding masters to FD
    FD_SET( master_udp, &read_fds );
    FD_SET( master_visu_tcp, &read_fds );
    FD_SET( master_tcp, &read_fds );

    // (re)adding visualisation client
    if( visu_tcp != -1 ){
      FD_SET( visu_tcp, &read_fds );
    }

    // (re)adding neighbours to FD
    for( it_p=0; it_p<MAX_NGB; it_p++){

      fdset_ngb = srv_array[my_srv_id].ngb[it_p];

      if( (fdset_ngb > -1) && (srv_array[fdset_ngb].link > -1) ){
        FD_SET( srv_array[fdset_ngb].link, &read_fds );
      }
    }

    // (re)setting timeout value
    select_timeout.tv_sec = MAIN_SELECT_TIMEOUT;
    select_timeout.tv_usec = 0;


    // ____________________ SELECT() ______________________ok


    select_ret = select( FD_SETSIZE, &read_fds, NULL, NULL, &select_timeout );

    if( select_ret == 0 ) {

      // fprintf(stderr, " Main select() : nothing happened, looping...\n");

    }
    else if( select_ret == -1 ) {

      if( errno == EBADF ){
        ERROR("there may be a bogus file descriptor in the set");
      }

      if ( errno != EINTR ) // if NOT due to interruption
        ERROR("select() failed (retrying)");

    }
    else {


      // _____________ HANDLING INCOMING SERVERS ______________ok


      int it_q = -1;
      socklen_t mtcp_tmp_size;
      int mtcp_tmp = -1;
      struct sockaddr_in mtcp_tmp_sockaddr;

      if( FD_ISSET( master_tcp, &read_fds) ){

        mtcp_tmp_size = sizeof( mtcp_tmp_sockaddr );
        mtcp_tmp = accept( master_tcp, (struct sockaddr *) &mtcp_tmp_sockaddr, &mtcp_tmp_size );

        if( mtcp_tmp == -1 )
          ERROR("accept() failed (ignoring)");

        fprintf(stderr, " Incoming server %s trying to connect...", inet_ntoa( mtcp_tmp_sockaddr.sin_addr ));

        it_q = 0;
        while( it_q < MAX_NGB ){ 

          if ( srv_array[my_srv_id].ngb[it_q] != -1 ){

            if( ( strcmp( (const char*) srv_array[ srv_array[my_srv_id].ngb[it_q] ].ip, 
                          (const char*) inet_ntoa( mtcp_tmp_sockaddr.sin_addr ) ) == 0 ) ){

              srv_array[ srv_array[my_srv_id].ngb[it_q] ].link = mtcp_tmp;
              srv_array[ srv_array[my_srv_id].ngb[it_q] ].sockaddr_link = mtcp_tmp_sockaddr; 
              fprintf(stderr, " granted :)\n");
              break;

            }

          }

          it_q++;

        }

        if( it_q == MAX_NGB ){

          close(mtcp_tmp);
          mtcp_tmp = -1;
          fprintf(stderr, " denied!\n");

        }

      }


      // ____________ HANDLING CLIENTS _______________ok


      int respo_id = -1;
      int it_sendto_ngb = -1;
      int fwd_ret = -1;
      int tmp_err404 = 404;
      char udp_dump[MAX_UDP_DATA];
      StatusPacket udp_packet;

      if( FD_ISSET( master_udp, &read_fds) ){

        fillret = Server_FillBufferfromNetw( master_udp, 
                                             udp_dump, 
                                             MAX_UDP_DATA, 
                                             (struct sockaddr*) &master_udp_addr );

        if( fillret > -1 ){
          packret = Server_FillStatusPacket( udp_dump,
                                             MAX_UDP_DATA,
                                             &udp_packet,
                                             inet_ntoa( master_udp_addr.sin_addr ) );

          if( packret > -1 ){

            // is this Client in my Zone ?
            if( Server_inZone( &srv_array[my_srv_id], &udp_packet.location ) == 0 ){

              fprintf(stderr, " Client '%s' is in my responsability zone\n", udp_packet.mac);
              updateret = Server_UpdateClient( cli_array, max_client_id, &udp_packet ); 

              if( updateret < 0 ){
                ERROR("could not update Client");
              }

              // is Client in a Shared Zone ?
              for( it_sendto_ngb = 0; it_sendto_ngb < MAX_NGB; it_sendto_ngb++ ){

                // is it worth considering this neighbour ?
                if( ( srv_array[my_srv_id].ngb[it_sendto_ngb] != -1 ) &&
                    ( srv_array[ srv_array[my_srv_id].ngb[it_sendto_ngb] ].link != -1 ) ) {

                  // is it really in the right zone ?
                  if( Server_inZone( &srv_array[ srv_array[my_srv_id].ngb[it_sendto_ngb] ], &udp_packet.location ) == 0 ){ 

                    // Yes, forward!
                    fwd_ret = Server_ForwardPackettoServer( &srv_array[ srv_array[my_srv_id].ngb[it_sendto_ngb] ], 
                                                            &udp_packet );

                    if( fwd_ret > -1 )
                      fprintf(stderr, 
                              " Client %s is in a shared zone, sent SP to server %s too\n",
                              udp_packet.mac,
                              srv_array[ srv_array[my_srv_id].ngb[it_sendto_ngb] ].ip );
                    else
                      ERROR("could not forward SP to neighbour");

                  } // end of inZone

                } 

              } // end of list my ngbs

            } // end of 'Client in my zone'

            // that Client is not in my Zone !  
            else {

              respo_id = Server_ResponsibleID( srv_array, max_srv_id, &udp_packet.location );

              // found a suitable Server!
              if( respo_id > -1 ){

                if( Server_ReplytoClientSP( &udp_packet, PROTOCOL_CLIENT_REDIRECT, srv_array[respo_id].ip ) > -1 ){

                  fprintf(stderr, 
                          " Client %s NOT in my respo zone : REDIRECT to server %s! \n",
                          udp_packet.mac,
                          srv_array[respo_id].ip);
                }
                else
                  ERROR("REDIRECT sending failed");

              }

              // are you one the same planet ?
              else {

                if( Server_ReplytoClientSP( &udp_packet, PROTOCOL_CLIENT_ERROR, &tmp_err404 ) > -1 )
                  fprintf(stderr, " Client %s NOT in my respo zone & server NOT found (sent ERROR)\n",
                          udp_packet.mac); 
                else
                  ERROR("ERROR packet sending failed (you'd better buy a CZ 75)");
              }

            }

          } else { 

            ERROR("could not fill SP, cannot even send ERROR packet back"); 

          }
        }
        else { ERROR("could not get data from network"); }

      }


      // ____________ HANDLING INCOMING VISU CLIENTS ________________ok


      socklen_t mvisu_tmp_size;
      int mvisu_tmp = -1;
      struct sockaddr_in mvisu_tmp_sockaddr;

      if( FD_ISSET( master_visu_tcp, &read_fds) ){

        mvisu_tmp_size = sizeof( mvisu_tmp_sockaddr );
        mvisu_tmp = accept( master_visu_tcp, (struct sockaddr *) &mvisu_tmp_sockaddr, &mvisu_tmp_size );

        if( mvisu_tmp == -1 )
          ERROR("accept() failed, ignoring");

        fprintf(stderr, " Incoming visualisation client %s trying to connect...", inet_ntoa( mvisu_tmp_sockaddr.sin_addr ));

        if( visu_tcp == -1 ){
          visu_tcp = mvisu_tmp;
          fprintf(stderr, " granted :)\n");

          if( Server_SendAuthoritytoVisu( visu_tcp, &srv_array[ my_srv_id ] ) > -1 )
            fprintf(stderr, " Sent 'Authority' packet to Visu client\n");
          else
            ERROR("could not send Authority packet to Visu client");

        }
        else {

          close(mvisu_tmp);
          mvisu_tmp = -1;
          fprintf(stderr, " denied!\n");

        }

      }


      // _____________ HANDLING ALIVE NEIGHBOURS _______________ok


      char ngb_dump[MAX_TCP_DATA];
      StatusPacket ngb_packet;

      for( it_i=0; it_i<MAX_NGB; it_i++ ){

        select_ngb = srv_array[my_srv_id].ngb[it_i];

        if( ( select_ngb > -1 ) &&
            ( srv_array[select_ngb].link > -1 ) && 
            ( FD_ISSET( srv_array[select_ngb].link, &read_fds ) ) ) {

          fillret = Server_FillBufferfromNetw( srv_array[select_ngb].link, 
                                               ngb_dump, 
                                               MAX_TCP_DATA, 
                                               NULL );

          if( fillret > -1 ){
            packret = Server_FillStatusPacket( ngb_dump,
                                               MAX_TCP_DATA,
                                               &ngb_packet,
                                               srv_array[select_ngb].ip );

            if( packret > -1 ){

              // is this Client in my authority zone ?
              if( Server_inZone( &srv_array[my_srv_id], &ngb_packet.location ) == 0 ) {

                updateret = Server_UpdateClient( cli_array, max_client_id, &ngb_packet ); 

                if( updateret < 0 ){
                  ERROR("could not update Client");
                }

              } else { 
                fprintf( stderr, 
                         " Ignored 1 Status Packet from neighbour %s (received a SP NOT in my authority zone)\n",
                         srv_array[select_ngb].ip ); 
              }

            } else { ERROR("could not fill SP"); }

          } else { 

            FD_CLR( srv_array[select_ngb].link, &read_fds );
            close( srv_array[select_ngb].link );
            srv_array[select_ngb].link = -1;
            fprintf(stderr, " Could not get data from network, let's assume that neighbour %s is dead\n",
                    srv_array[select_ngb].ip); 

          }

        } // end of if FDISSET on neighbours 

      } // end of ngb listing


      // _____________ HANDLING VISU CLIENT _______________ok


      char visu_dump[MAX_VISU_DATA];
      int visu_goodbye_value = -1;

      if( ( visu_tcp != -1 ) &&
          ( FD_ISSET( visu_tcp, &read_fds ) ) ){

        fillret = Server_FillBufferfromNetw( visu_tcp, 
                                             visu_dump, 
                                             MAX_TCP_DATA, 
                                             (struct sockaddr*) NULL );

        if( fillret == -1 ){
          FD_CLR( visu_tcp, &read_fds );
          close(visu_tcp);
          visu_tcp_ready = INVALID_CLIENT;
          visu_tcp = -1;
          fprintf(stderr, " Could not get data from network, let's assume that visu client is dead"); 
        }
        else{

          if( strcmp( visu_dump, PROTOCOL_VISU_CS_START ) == 0 ){
            visu_tcp_ready = VALID_CLIENT;
            fprintf(stderr, " Visu client is now ready\n");
          }
          if( sscanf( visu_dump, PROTOCOL_VISU_CS_GOODBYE, &visu_goodbye_value ) == 1 ){
            FD_CLR( visu_tcp, &read_fds );
            close(visu_tcp);
            visu_tcp_ready = INVALID_CLIENT;
            visu_tcp= -1;
            fprintf(stderr, " Visu Client is gone (returned: %d)\n", visu_goodbye_value);
          }
        }
      }

    } // end of successfull select


    // ______________ DEAD NEIGHBOURS REVIVAL __________________ok


    // listing my neighbours
    for( it_j=0; it_j<MAX_NGB; it_j++ ){

      srv_to_link_id = srv_array[my_srv_id].ngb[it_j];

      if( srv_to_link_id == -1 )
        continue;

      if( srv_array[ srv_to_link_id ].link == -1 ){

        if( Server_NgbTcpLink( &srv_array[my_srv_id], &srv_array[srv_to_link_id] ) == -1 ){
          ERROR("failed to contact neighbour (skipping)");
        }

      }

    }


  } // end of main for() 


  // ________________ CLEAN EXIT ________________ok


  // closing inter server TCP links
  int it_l = -1;
  int it_m = -1;
  int tmp_sd = -1;

  for( it_l=0; it_l<max_srv_id; it_l++ ){

    for( it_m=0; it_m<MAX_NGB; it_m++ ){

      if( srv_array[ it_l ].ngb[ it_m ] > -1 ){

        tmp_sd = srv_array[ srv_array[ it_l ].ngb[ it_m ] ].link; 

        if( tmp_sd > -1 ) { 
          close( tmp_sd );
        }
      }  
    }
  }

  if( visu_tcp > -1 )
    close(visu_tcp);

  close(master_tcp);
  close(master_udp);
  close(master_visu_tcp);
  fprintf(stderr, " Performed clean exit, goodbye.\n");
  return(0);

}

